The overdetermination of Western chords and scales leads ... composers ... [that are] interested in harmonic consistency, acoustic consonance, or scalar transposition ... necessarily ... toward the same familiar musical objects.(Dmitri Tymoczko, A Geometry of Music, chap 4, page 123)
This notebook is a demonstration of Orbichord, a project to explore topologically non-trivial space of music chords that are global-quotient orbifolds, see references:
Orbichord comes from combining the words orbifold and chord. It is a collection of python modules build on top of music21 project.
Import music and graphite modules
# Import music modules
from music21.harmony import chordSymbolFigureFromChord
from music21.interval import Interval
from music21.scale import MajorScale
from music21.stream import Stream
from numpy import inf
from numpy import linalg as la
from orbichord.chordinate import EfficientVoiceLeading
from orbichord.graph import createGraph, convertGraphToData
from orbichord.generator import Generator
from orbichord.utils import renderWithLily, playAudio
import networkx as nx
# Import graphic modules
import pandas as pd
import holoviews as hv
from holoviews import opts, dim
from bokeh.sampledata.les_mis import data
hv.extension('bokeh')
hv.output(size=180)
defaults = dict(width=300, height=300, padding=0.1)
hv.opts.defaults(
opts.EdgePaths(**defaults), opts.Graph(**defaults), opts.Nodes(**defaults))
Create a chord generator using seven pitches of the C major scale. By default, chords are identified by the popular name with no inversion, resulting in the space chord defined by a collection pitch-class sets). We also select those triad chords, resulting in a generator for C major triad chords.
scale = MajorScale('C')
chord_generator = Generator(
pitches=scale.getPitches('C','B'),
)
Instantiate an efficient voice leading object using the C major scale to define voice leading steps. Define the metric as the maximum of absolute number of steps between all the voices or $L_{\infty}$ norm). All non-crossing voice leading between two chords are explored using interscalar matrix computed in polinomial time by algorithm descrived in Dmitri Tymoczko, A Geometry of Music, Appendix D, page 420.
max_norm_vl = EfficientVoiceLeading(
scale = scale,
metric = lambda delta: la.norm(delta, inf)
)
Use a chord graph to explore how chords are connected. For this, you need to pass a generator and voice leading objects, as well as a tolerance function. The tolerance function provides the criteria to select how close chords can be to be considered an efficient voice leading.
graph, node_to_chord = createGraph(
generator = chord_generator,
voice_leading = max_norm_vl,
tolerance = lambda x: x <= 1.0
)
The output of the function is a networkx graph and a dictionary to map node to their counterpart chord object.
Convert the chord networkx graph into a collection of links and nodes.
edges, vertices = convertGraphToData(graph)
links = pd.DataFrame(edges)
nodes = hv.Dataset(pd.DataFrame(vertices), 'index')
print(links.head())
nodes.data.head()
Visualize chord graph using (ironically) a chord graph.
chord = hv.Chord((links, nodes))
chord.opts(
opts.Chord(
cmap='Category20',
edge_cmap='Category20',
edge_color=dim('source').str(),
labels='name', node_color=dim('index').str()
)
)
We can also visualized the graph directly.
gview = hv.Graph.from_networkx(graph, nx.layout.circular_layout)
gview.opts(node_size=40)
labels = hv.Labels(gview.nodes, ['x', 'y'], 'index')
(gview * labels.opts(text_font_size='10pt', text_color='white', bgcolor='white'))
All the triad chords in the space of pitch-class sets can be connected by an efficient voice leading. This voice leading can change multiple pitches in the chord but not more that one scale step on each pitch. However, naively we find that, for example, that G major seems to be transposed 5 scale steps from C major.
# Set a step to bring all pitchs to a treble clef
interval4 = Interval(4*12)
# Set the central node
node = 'C'
# Collect its neigbors
neighbors = [node]
for neighbor, edge in graph.adj[node].items():
neighbors.append(neighbor)
# Sort chords based on their named pitch
neighbors.sort(key = lambda name: name[0])
size = len(neighbors)
# Cyclic permutations to start for C major cord
neighbors = [neighbors[(i+2)%size] for i in range(size)]
# Define a stream of chords
stream = Stream()
for neighbor in neighbors:
chord = node_to_chord[neighbor].transpose(interval4)
chord.addLyric(chordSymbolFigureFromChord(chord))
chord.duration.type = 'whole'
stream.append(chord)
# Render the resulting chord progression
renderWithLily(stream)
# Play the chord progression
playAudio(stream)
But then, in what sense C and G major chords are only one step of the major scale?
To understand this, you need to realize that for each chord, there is six possible permutations (three inversion x two permutations for each inversion). In the space of pitch-class sets, all these permutations are identified as the same musical object. Therefore, a better understanding of distances between triad requires accounting for chord permutation as detailed in another notebook.